package com.tn.rtdata.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.tn.rtdata.DataEvent;
import com.tn.rtdata.DataListener;
import com.tn.rtdata.DataSource;
import com.tn.rtdata.FillException;

/**
 * An implementation of <code>Map</code> that listens for <code>DataEvent</code>s from a <code>DataSource</code> updating its contents
 * accordingly.
 */
public class LiveMap<K, V> implements ObservableMap<K, V>
{
  private static final Logger LOGGER = LoggerFactory.getLogger(LiveMap.class);

  private DataListener<V> dataListener;
  private List<DataListener<V>> dataListeners;
  private DataSource<V> dataSource;
  private Map<K, V> storage;
  private Function<K, V> keyGetter;

  /**
   * Creates a new <code>LiveMap</code>.
   *
   * @param dataSource the data source to listen to.
   * @param storage the storage used to hold the data returned from the data source.
   */
  public LiveMap(DataSource<V> dataSource, Map<K, V> storage, Function<K, V> keyGetter) throws FillException
  {
    this.dataSource = dataSource;
    this.storage = storage;
    this.keyGetter = keyGetter;

    this.dataListener = new SynchronizingDataListener();
    this.dataSource.addDataListener(this.dataListener);

    this.dataListeners = new ArrayList<DataListener<V>>();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void finalize() throws Throwable
  {
    dataSource.removeDataListener(dataListener);
    super.finalize();
  }

  /**
   * Starts this <code>LiveMap</code>.
   */
  public void start() throws FillException
  {
    dataSource.start();
  }

  /**
   * Stops this <code>LiveMap</code>.
   */
  public void stop() throws FillException
  {
    dataSource.stop();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int size()
  {
    return storage.size();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isEmpty()
  {
    return storage.isEmpty();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean containsKey(Object key)
  {
    return storage.containsKey(key);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean containsValue(Object value)
  {
    return storage.containsValue(value);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public V get(Object key)
  {
    return storage.get(key);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public V put(K key, V value)
  {
    throw new UnsupportedOperationException();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public V remove(Object key)
  {
    throw new UnsupportedOperationException();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void putAll(Map<? extends K, ? extends V> map)
  {
    throw new UnsupportedOperationException();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void clear()
  {
    throw new UnsupportedOperationException();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Set<K> keySet()
  {
    return storage.keySet();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Collection<V> values()
  {
    return storage.values();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Set<Entry<K, V>> entrySet()
  {
    return storage.entrySet();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void addDataListener(DataListener<V> dataListener)
  {
    dataListeners.add(dataListener);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void removeDataListener(DataListener<V> dataListener)
  {
    dataListeners.remove(dataListener);
  }

  /**
   * Fires the <code>dataListeners</code>.
   */
  private void fireDataListeners(DataEvent<V> dataEvent)
  {
    for (DataListener<V> dataListener : new ArrayList<DataListener<V>>(dataListeners))
    {
      dataListener.dataEvent(dataEvent);
    }
  }

  /**
   * An implementation of <code>DataListener</code> that updates the <code>storage</code> <code>Map</code>.
   */
  private class SynchronizingDataListener implements DataListener<V>
  {
    /**
     * {@inheritDoc}
     */
    @Override
    public void dataEvent(DataEvent<V> dataEvent)
    {
      switch (dataEvent.getType())
      {
        case INITIAL:
        case ADD:
          storage.put(keyGetter.execute(dataEvent.getNewValue()), dataEvent.getNewValue());
          fireDataListeners(dataEvent);
          return;

        case UPDATE:
          storage.remove(keyGetter.execute(dataEvent.getOldValue()));
          storage.put(keyGetter.execute(dataEvent.getNewValue()), dataEvent.getNewValue());
          fireDataListeners(dataEvent);
          return;

        case DELETE:
          storage.remove(keyGetter.execute(dataEvent.getOldValue()));
          fireDataListeners(dataEvent);
          return;

        case RESET:
          storage.clear();
          fireDataListeners(dataEvent);
          return;

        default:
          LOGGER.warn("Unrecognized data event type: ", dataEvent.getType());
      }
    }
  }
}
